Skip to content

feat: add PHP framework integration primitives - #282

Merged
marandaneto merged 6 commits into
mainfrom
feat/php-framework-integration
Oct 29, 2026
Merged

marandaneto merged 6 commits into
mainfrom
feat/php-framework-integration

Conversation

@marandaneto

@marandaneto marandaneto commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

Additive public API (maintainer-approved): Client::captureException() and the static facade accept options['source'], emitted as event-level $exception_source. It is independent of semantic mechanism.type and handled; it never sets nested mechanism.source. Invalid/missing typed source is omitted, and generic additional/ambient property bags cannot supply provenance.

💡 Motivation and Context

Framework integrations need to keep context active across before/after callbacks, report failed boundaries accurately, and use a client instance without changing the global default.

This is the first PR in a Laravel integration stack. It adds reusable PHP SDK support only:

  • Add Client::groupIdentify() and delegate the static facade to it.
  • Add enterContext() and an idempotent closeable scope. Scopes preserve client and Fiber isolation, including cleanup from another execution path.
  • Allow framework exception capture to supply handled/mechanism metadata while preserving the original exception chain and stack. Existing manual capture stays handled by default.
  • Add @internal protected Client::libraryName(), libraryVersion() and releaseId() overrides for PostHog framework integrations (library metadata, and release IDs from cached application configuration). These are not constructor options, so customers keep POSTHOG_RELEASE_ID as the only documented release ID input. Event properties still take precedence.

Public API changes are recorded in the API snapshot. Capture, group-identify, exception metadata and tracing-header specs apply. The optional framework boundary metadata extends the manual exception API without changing its default handled behavior.

💚 How did you test it?

  • Added framework tests first and confirmed the missing APIs and handled-state assertion failed before implementation.
  • Full PHP suite after the human review follow-up: 2,532 tests, 11,366 assertions.
  • Focused framework and configured batch transport suites: 48 tests, 211 assertions.
  • PHP_CodeSniffer for all changed PHP files, API snapshot check and git diff --check pass.
  • Initial isolated autoreview of 34e07100d9ce06f2c56312a9d1072a59574ffed8 against origin/main reported no actionable findings.

Review follow-up 8773c43 addresses all three bot findings with failing-before/passing-after regressions: configured batch User-Agents (including safe shell/header handling), abandoned Fiber cleanup without retention or object-ID identity reuse, and independently validated exception metadata overrides. The 16 adapter tests also pass. These fixes follow the HTTP client and exception-event-metadata contracts; unrelated Retry-After compliance report failures remain outside this framework PR.

📝 Checklist

  • All commits are signed.
  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed.
  • No breaking change or entry added to the changelog.

If releasing new changes

  • Ran pnpm change to generate a change intent file.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Implemented with Pi using local file tools, PHP/Composer, Git and GitHub CLI. A separate isolated Pi review checked the final core diff. The user requested an independently released Laravel package depending on the PHP SDK, with stacked PRs and an example application. Framework lifecycle and payload support live in the core so the package does not duplicate them. Human review is required before merge.

Stack

Merge bottom-up:

  1. feat: add PHP framework integration primitives #282: PHP framework support
  2. feat: add the Laravel runtime package #283: Laravel runtime
  3. feat: add opt-in Laravel autocapture #284: Optional server autocapture
  4. feat: integrate PostHog feature flags with Laravel Pennant #285: Read-only Pennant integration
  5. feat: add a runnable Laravel SDK example #286: Runnable Laravel example
  6. ci: release the Laravel package independently #287: Independent CI and publishing

Laravel source lives in this repository but its Composer package is released separately. Publish core ^4.20 first. The distribution repository, protected release environment and Packagist setup remain administrator prerequisites. No release has been published.

Follow-up ecbc8cc supplies the internal context-bound snapshot host needed by #283's lazy-exposure feedback. Four regressions failed before the fix (later-session leakage and unscoped snapshots inheriting later context), then passed; the combined flag/framework suites pass 491 tests / 1,899 assertions. A fresh read-only review of this exact commit found no actionable issues. API shape is unchanged. PHP 8.2–8.5, coverage, style, API, and security CI checks pass.

Human review follow-up 858c204

  • Invalid instance group identity now logs a validation warning and returns false, without enqueueing a group event. Static-facade type/key validation keeps its original exceptions/messages. Six real SDK throw reproductions are now green.
  • The typed source channel follows the pinned exception metadata contract: e.g. mechanism=middleware, source=laravel.middleware, handled=false. No derivation between these independent fields or guessing from libraries/stack frames. Eleven source regressions failed before (ignored typed metadata/property-bag spoofing), then passed, including normal static forwarding, invalid override omission, context/session preservation and before_send provenance.
  • Scope class/close and both enterContext docs explain retained ownership, immediate destruction if discarded, idempotence and younger-scope preservation. A retained regression confirms discard behavior; existing lifecycle/Fiber cases remain green.
  • Nineteen tests were added, plus default-library provenance assertions. Focused 20 tests / 176 assertions; full SDK 2,532 / 11,366. API snapshot remains current because PHP signatures are unchanged; the new array option is documented explicitly. The existing minor change intent covers framework metadata. Scoped style checks pass with zero new warnings; the legacy PostHogTest file retains the same 23 existing non-fixable warnings. Independent isolated review is clean.
  • Signed bottom-up merges preserve the complete Laravel stack. Final committed-source Laravel 12/13 matrices each pass 106 package tests / 664 assertions and 29 example tests / 163 assertions; all 13 release-tooling tests pass.
  • This core follow-up adds the explicit channel; existing Laravel forwarding can pass it. Automatic Laravel hook-specific source selection is not claimed as implemented here.

Before the first Laravel release: server SDK registration

posthog-laravel is not yet registered on PostHog server master for PHP usage attribution, Python/Rust feature-flag library classification/parser, or SDK health frontend/backend registries. A separate backend registration/alias PR must land before the Laravel package ships, following the approach in PostHog/posthog#93911. Event ingestion itself is not rejected just because this library identifier is unclassified. SDK health/version handling must respect Laravel's independent package version, not compare 0.1.x against PHP core 4.x. No backend PR, repository or release was created by this SDK follow-up; this remains an explicit pre-release prerequisite alongside core ^4.20 publication and distribution/Packagist setup.

@marandaneto marandaneto self-assigned this Oct 29, 2026

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostHog Review · Standard · reviewed 34e0710: 3 Should fix.

This is the only automatic review while this pull request is a draft. Comment @posthog review to review new changes sooner. Otherwise, a review of the new changes runs when you mark it ready for review.

See this review and its findings in PostHog Review. Comment @posthog review --deep for a Deep review. A Deep review takes longer and looks deeper than a Standard review.

Comment thread lib/Client.php Resolved
Comment thread lib/RequestContext.php Resolved
Comment thread lib/Client.php Resolved
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

posthog-php-fork_curl Compliance Report

Date: 2026-10-29T16:11:15.765301+00:00
Duration: 116327ms

⚠️ Some Tests Failed

46/47 tests passed, 1 failed


Capture Tests

⚠️ 29/30 tests passed, 1 failed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 34ms
Format Validation.Event Has Uuid ✅ 527ms
Format Validation.Event Has Lib Properties ✅ 529ms
Format Validation.Distinct Id Is String ✅ 529ms
Format Validation.Token Is Present ✅ 529ms
Format Validation.Custom Properties Preserved ✅ 527ms
Format Validation.Event Has Timestamp ✅ 529ms
Format Validation.Non Utc Event Timestamp Is Converted To Utc ✅ 529ms
Retry Behavior.Retries On 503 ✅ 5850ms
Retry Behavior.Does Not Retry On 400 ✅ 2535ms
Retry Behavior.Does Not Retry On 401 ✅ 2531ms
Retry Behavior.Respects Retry After Header ❌ 6345ms
Retry Behavior.Implements Backoff ✅ 16273ms
Retry Behavior.Retries On 500 ✅ 5647ms
Retry Behavior.Retries On 502 ✅ 5647ms
Retry Behavior.Retries On 504 ✅ 5645ms
Retry Behavior.Max Retries Respected ✅ 16274ms
Deduplication.Generates Unique Uuids ✅ 536ms
Deduplication.Preserves Uuid On Retry ✅ 5645ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 10859ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 5644ms
Deduplication.No Duplicate Events In Batch ✅ 535ms
Deduplication.Different Events Have Different Uuids ✅ 530ms
Compression.Sends Gzip When Enabled ✅ 531ms
Batch Format.Uses Proper Batch Structure ✅ 529ms
Batch Format.Flush With No Events Sends Nothing ✅ 517ms
Batch Format.Multiple Events Batched Together ✅ 518ms
Error Handling.Does Not Retry On 403 ✅ 2533ms
Error Handling.Does Not Retry On 413 ✅ 2531ms
Error Handling.Retries On 408 ✅ 5646ms

Failures

retry_behavior.respects_retry_after_header

Retry delay too short: 809ms < 2500ms

Feature_Flags Tests

✅ 17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 522ms
Request Payload.Flags Request Uses V2 Query Param ✅ 520ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 522ms
Request Payload.Flags Request Omits Authorization Header ✅ 521ms
Request Payload.Token In Flags Body Matches Init ✅ 521ms
Request Payload.Groups Round Trip ✅ 521ms
Request Payload.Groups Default To Empty Object ✅ 520ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 521ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 521ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 521ms
Request Lifecycle.No Flags Request On Init Alone ✅ 516ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 517ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 523ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 523ms
Retry Behavior.Retries Flags On 502 ✅ 624ms
Retry Behavior.Retries Flags On 504 ✅ 622ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 533ms

@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

posthog-php-lib_curl Compliance Report

Date: 2026-10-29T16:11:15.078340+00:00
Duration: 116513ms

⚠️ Some Tests Failed

46/47 tests passed, 1 failed


Capture Tests

⚠️ 29/30 tests passed, 1 failed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 26ms
Format Validation.Event Has Uuid ✅ 521ms
Format Validation.Event Has Lib Properties ✅ 522ms
Format Validation.Distinct Id Is String ✅ 523ms
Format Validation.Token Is Present ✅ 522ms
Format Validation.Custom Properties Preserved ✅ 522ms
Format Validation.Event Has Timestamp ✅ 522ms
Format Validation.Non Utc Event Timestamp Is Converted To Utc ✅ 522ms
Retry Behavior.Retries On 503 ✅ 5832ms
Retry Behavior.Does Not Retry On 400 ✅ 2524ms
Retry Behavior.Does Not Retry On 401 ✅ 2525ms
Retry Behavior.Respects Retry After Header ❌ 6332ms
Retry Behavior.Implements Backoff ✅ 16245ms
Retry Behavior.Retries On 500 ✅ 5633ms
Retry Behavior.Retries On 502 ✅ 5631ms
Retry Behavior.Retries On 504 ✅ 5630ms
Retry Behavior.Max Retries Respected ✅ 17045ms
Deduplication.Generates Unique Uuids ✅ 234ms
Deduplication.Preserves Uuid On Retry ✅ 5630ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 10839ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 5634ms
Deduplication.No Duplicate Events In Batch ✅ 529ms
Deduplication.Different Events Have Different Uuids ✅ 524ms
Compression.Sends Gzip When Enabled ✅ 524ms
Batch Format.Uses Proper Batch Structure ✅ 525ms
Batch Format.Flush With No Events Sends Nothing ✅ 519ms
Batch Format.Multiple Events Batched Together ✅ 512ms
Error Handling.Does Not Retry On 403 ✅ 2525ms
Error Handling.Does Not Retry On 413 ✅ 2525ms
Error Handling.Retries On 408 ✅ 5631ms

Failures

retry_behavior.respects_retry_after_header

Retry delay too short: 803ms < 2500ms

Feature_Flags Tests

✅ 17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 524ms
Request Payload.Flags Request Uses V2 Query Param ✅ 521ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 521ms
Request Payload.Flags Request Omits Authorization Header ✅ 521ms
Request Payload.Token In Flags Body Matches Init ✅ 521ms
Request Payload.Groups Round Trip ✅ 521ms
Request Payload.Groups Default To Empty Object ✅ 523ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 521ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 521ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 521ms
Request Lifecycle.No Flags Request On Init Alone ✅ 517ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 508ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 525ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 523ms
Retry Behavior.Retries Flags On 502 ✅ 625ms
Retry Behavior.Retries Flags On 504 ✅ 623ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 525ms

@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

posthog-php-socket Compliance Report

Date: 2026-10-29T16:11:08.584767+00:00
Duration: 116158ms

⚠️ Some Tests Failed

46/47 tests passed, 1 failed


Capture Tests

⚠️ 29/30 tests passed, 1 failed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 28ms
Format Validation.Event Has Uuid ✅ 523ms
Format Validation.Event Has Lib Properties ✅ 524ms
Format Validation.Distinct Id Is String ✅ 524ms
Format Validation.Token Is Present ✅ 524ms
Format Validation.Custom Properties Preserved ✅ 524ms
Format Validation.Event Has Timestamp ✅ 525ms
Format Validation.Non Utc Event Timestamp Is Converted To Utc ✅ 524ms
Retry Behavior.Retries On 503 ✅ 5833ms
Retry Behavior.Does Not Retry On 400 ✅ 2527ms
Retry Behavior.Does Not Retry On 401 ✅ 2527ms
Retry Behavior.Respects Retry After Header ❌ 6332ms
Retry Behavior.Implements Backoff ✅ 16247ms
Retry Behavior.Retries On 500 ✅ 5632ms
Retry Behavior.Retries On 502 ✅ 5631ms
Retry Behavior.Retries On 504 ✅ 5633ms
Retry Behavior.Max Retries Respected ✅ 16249ms
Deduplication.Generates Unique Uuids ✅ 532ms
Deduplication.Preserves Uuid On Retry ✅ 5631ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 10840ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 5632ms
Deduplication.No Duplicate Events In Batch ✅ 532ms
Deduplication.Different Events Have Different Uuids ✅ 526ms
Compression.Sends Gzip When Enabled ✅ 524ms
Batch Format.Uses Proper Batch Structure ✅ 524ms
Batch Format.Flush With No Events Sends Nothing ✅ 520ms
Batch Format.Multiple Events Batched Together ✅ 512ms
Error Handling.Does Not Retry On 403 ✅ 2527ms
Error Handling.Does Not Retry On 413 ✅ 2527ms
Error Handling.Retries On 408 ✅ 5630ms

Failures

retry_behavior.respects_retry_after_header

Retry delay too short: 802ms < 2500ms

Feature_Flags Tests

✅ 17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 526ms
Request Payload.Flags Request Uses V2 Query Param ✅ 521ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 522ms
Request Payload.Flags Request Omits Authorization Header ✅ 523ms
Request Payload.Token In Flags Body Matches Init ✅ 522ms
Request Payload.Groups Round Trip ✅ 524ms
Request Payload.Groups Default To Empty Object ✅ 522ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 522ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 524ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 522ms
Request Lifecycle.No Flags Request On Init Alone ✅ 518ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 508ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 526ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 523ms
Retry Behavior.Retries Flags On 502 ✅ 626ms
Retry Behavior.Retries Flags On 504 ✅ 624ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 525ms

Remove library_name, library_version and release_id from the public client
options. Framework integrations override internal Client methods instead, so
customers keep POSTHOG_RELEASE_ID as the only documented release id input.
@marandaneto
marandaneto marked this pull request as ready for review October 29, 2026 12:24
@marandaneto
marandaneto requested a review from a team as a code owner October 29, 2026 12:24

@turnipdabeets turnipdabeets left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. The primitives map cleanly onto Node's enterContext / getLibraryId() and every one has a consumer in #283. A few non-blocking notes inline.

Comment thread lib/Client.php Resolved
Comment thread lib/Client.php Resolved
Comment thread lib/Client.php
*
* @internal Overridden by PostHog framework integrations; not part of the customer API.
*/
protected function libraryName(): string

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[question] Once #283 reports $lib = posthog-laravel, the server won't recognise it: the usage report php_events bucket, the flags Library enum and the SDK health list only know posthog-php. Do we want an alias PR in posthog (like PostHog/posthog#93911 for rails/aspnetcore) before the Laravel package ships?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes—a separate server registration/alias PR is needed before the Laravel package ships. Rechecked current PostHog/posthog master: posthog-laravel is absent from PHP usage attribution, the Python/Rust flag library registries/parser, and SDK-health frontend/backend registries. This is an attribution/classification gap, not ingestion rejecting events with the new $lib. I recorded it explicitly as a pre-release prerequisite in #282–#287, referencing #93911. SDK-health/version handling must use Laravel's independent 0.1.x package version, not compare it against PHP core 4.x. It does not block the standalone core release, whose default identity remains posthog-php. No backend PR was opened in this SDK follow-up; that separate work remains required before Laravel release.

Comment thread lib/ContextScope.php Resolved
@marandaneto
marandaneto merged commit 1184480 into main Oct 29, 2026
28 checks passed
@marandaneto
marandaneto deleted the feat/php-framework-integration branch October 29, 2026 17:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants